int x[10];
float v[]={1.0, 2.0, 3.0}, w[]={7.0, 8.0, 9.0}
C++에서 배열의 크기는 일정해야 하며, 컴파일 시 알고 있어야 한다.
gcc와 같은 일부 컴파일러는 런타임 크기를 지원
고 차원 배열 선언
float A[7][9];
int q[3][2][3];
C++은 배열에 선형 대수 연산을 제공하지 않는다.(행렬 곱)
void vector_add(unsigned size, const double v1[], const double v2[], double s[]){
for(unsigned i=0; i<size; ++i) s[i]=v1[i]=v2[i];
}
int main(void){
double x[]={2, 3, 4}, y[]={4, 2, 0}, sum[3];
vector_add(3, x, y, sum);
return 0;
}
함수로 배열을 넘겨준 경우, 크기 정보를 포함하기 때문에, 배열의 크기를 함수의 매개변수로 전달해야 한다.혹은, sizeof x/sizeof x[0]을 이용해서 배열의 크기(개수)를 알 수 있음
배열은 접근하기 전에 인덱스를 검사하지 않기 때문에 배열 외부에 접근한다는 사실을 알게되면,세그먼트 오류로 프로그램에서 크래시가 발생할 수 있음->새로운 배열 타입 사용
배열의 크기는 컴파일 타임에 알고 있어야 한다.
(ex 파일에 저장되어 있는 배열을 메모리로 읽어옴)->동적 메모리 할당 사용
ifstream ifs("som_array.dat");
ifs>>size;
float v[size];
포인터(pointer)포인터는 메모리 주소를 포함하는 변수이다.
주소 연산자(&x) 혹은 동적으로 할당한 메모리를 대입해서 사용
포인터는 개체를 참조하고, 동적 메모리를 관리할 때 사용된다.
동적 메모리 할당
int* y=new int[10];
delete[] y;
배열로 동적으로 할당하지 못하는 문제를 해결할 수 있다.
ifstream ifs("some_array.dat");
int size;
ifs>>size;
float* v=new float[size];
for(int i=0; i<size; ++i){
ifs>>v[i];
}
delete[] v;
포인터 또한 배열과 동일하게 범위를 벗어난 데이터에 접근하면, 프로그램에 충돌이 발생하거나, 데이터가 무효화 될 수 있다.
원하는 위치에 메모리 동적 할당
void* address=(void*)0xFF;
int* ip=new(address) int(5);
함수의 매개변수가 배열인 경우, C와 동일하게 내부적으로 포인터로 취급한다.
따라서 아래와 같이 배열인자로 정의된 함수에 포인터를 넘겨줄 수 있다.
void vector_add(unsigned size, const double v1[], const double v2[], double s[]){
for(unsigned i=0; i<size; ++i) s[i]=v1[i]=v2[i];
}
int main(int argc, char** argv){
double *x=new double[3], *y=new double[3], *sum=new double[3];
for(unsigned i=0; i<3; ++i){
x[i]=i+2, y[i]=4.2*i;
}
vector_add(3, x, y, sum);
return 0;
}
포인터를 사용하면, sizeof 트릭을 사용할 수 없다.(배열과 달리)
sizeof 포인터를 할 경우, 포인ㄴ터의 바이트 크기(32bit 체계에서 4byte)를 제공할 것임
단일 포인터 할당/해제 & 배열 포인터 할당/해재단일 포인터와 배열 포인터 해제를 잘못 처리하면, 런타임 시스템이 해제를 잘못 처리해
크래시가 발생할 가능성이 큼
int* ip1=new int;
delete ip;
int* ip2=new int[10];
delete[] ip2;
포인터 다른 변수 참조& 연산자는 개체를 가져와서 주소를 반환한다.
* 연산자는 주소를 가져와서 개체를 반환한다.
(C와 동일하게 포인터 동작)
int i=3;
int* ip3=&i;
int j=*ip3;
초기화 되지 않은 포인터(nullptr)초기화 되지 않은 포인터는 무작위 값이 할당된다.(오류 발생 가능)
아래처럼 초기화 되지 않은 포인터는 이를 명시할 수 있다.
int* ip4=nullptr;
int* ip5{};
int* ip4=0;
int* ip5=NULL;
주소값 0은 애플리케이션에서 절대 사용되지 않기 때문에 포인터가 비어 있다는 것을 나타내기 안전하다.
하지만 리터럴 0이 의도를 명확하게 나타내지 않기 때문에 오버로딩에서 모호함을 유발할 수 있음
NULL 매크로 또한 0으로 취환되기 때문에 동일하다.
nullptr로 초기화 한 포인터는 이후 모든 타입에 할당하거나, 비교할 수 있음
메모리 누수이미 할당된 포인터에 새로운 메모리를 할당한 경우,
이전의 할당된 메모리의 값을 다시 찾을 수 없으며 메모리 누수가 발생한다.
(이전 메모리 주소를 알 수 없기 때문에 해제도 불가능)
int* y=new int[10];
int* y=new int[10];
포인터 관련 오류를 최소화 하는 방법- 표준 컨테이너 사용
표준 라이브러리 std::vector는 크기 조정 및 범위 검사를 포함한 동적 배열의 모든 기능을 제공하며, 메모리를 자동 해제한다.
- 캡슐화
클래스에서 동적 메모리를 관리한다.
개체 생성 시, 메모리를 할당하고, 개체를 소멸할 때 해제: RAII(Resorce Acquistion Is Intialization)
- 스마트 포인터를 사용
원시 포인터(Raw Pointer)의 문제점은 해당 포인터가 데이터를 참조하고 있는지 아닌지를 알 수 없다는 점이다.
스마트 포인터를 사용하면, 이러한 구분을 타입 수준에서 명시 가능하다.
스마트 포인터<memory> 헤더에 정의
C++11에서 새로운 스마트 포인터 3가지가 도입되었다.
unique_ptr
shared_ptr
weak_ptr
(C++03에 기존의 auto_ptr은 더이상 사용하지 않는 것이 좋음)
unique_ptr참조한 데이터의 고유 소유권(Unique Ownership)을 나타낸다.
기본적으로 일반 포인터와 같이 사용할 수 있다.
#include <memory>
int main(void){
unique_ptr<double> dp{new double};
*dp=7;
...
}
생성자로 메모리를 할당하지 않을 경우 make_unique를 통해서 메모리 할당할 수 있다.
#include <memory>
int main(void){
std::unique_ptr<double> dp=std::make_unique<double>(5);
}
원시 포인터와 달리 포인터가 만료되면 자동으로 메모리를 해제한다.
(동적으로 할당하지 않은 주소를 할당하면 버그가 발생한다.)
double d;
unique_ptr<double> dd(&d);
unique_ptr는 다른 포인터 타입에 할당되거나 암시적으로 변환할 수 없다.원시 포인터에서 unique_ptr 포인터의 데이터를 얻고 싶다면 .get() 메서드를 사용한다.
다른 unique_ptr에 할당할 수 없다.
unique_ptr<double> dp2(dp);
dp2=dp;
unique_ptr에서는 move(소유권 이전)만 가능하다.
unique_ptr<double> dp2(move(dp)), dp3;
dp3=move(dp2);
dp1, dp2는 소유권을 이전한 이 후, nullptr이 되고
dp3의 소멸자는 메모리를 해제한다.(포인터가 소멸되는 것은 아님, 할당된 메모리를 해제함)
unique_ptr를 함수에서 반환할 때 메모리의 소유권을 전달한다.
이 때 함수의 결과(할당될 주소)는 임시값으로 소유권이 없기에 move가 필요 없다.
std::unique_ptr<double> f(void){
return std::unique_ptr<double>{new double};
}
int main(void){
unique_ptr<double> dp3;
dp3=f();
}
unique_ptr 배열
unique_ptr로 생성한 배열은 * 연산자를 통해서 접근할 수 없다.
unique_ptr<double[]> da(new double[3]);
for(unsigned i=0; i<3; ++i) da[i]=i+2;
unique_ptr은 원시 포인터에 비해 시간과 메모리에 대한 오버헤드가 전혀 없다(내부적으로 원시 포인터와 동일하게 구현, 거의 비슷함)
reset() 메서드
reset() 메서드를 통해서 unique_ptr에 할당된 메모리를 해제할 수 있다.
unique_ptr은 사용자 정의 Deleter를 제공한다.
shared_ptrunique_ptr과 달리 여러 파티(각 파티는 포인터를 갖고 있음)에서 공통으로 사용하는 메모리를 관리한다.
shared_ptr이 더 이상 데이터를 참조하지 않는 즉시 메모리를 자동으로 해제한다.
(동시성; 모든 스레드가 스레드에 대한 접근이 끝나면 메모리를 자동으로 해제함)
unique_ptr과 달리 shared_ptr은 원하는 만큼 자주 복사할 수 있다.
shared_ptr<double> f(void){
shared_ptr<double> p1(new double);
shared_ptr<double> p2(new double), p3=p2;
cout<<"p3.use_count()= "<<p3.use_count()<<endl;
return p3;
}
int main(void){
shared_ptr<double> p=f();
cout<<"p.use_count()= "<<p.use_count()<<endl;
}
use_count() 메서드
해당 메모리를 참조하고 있는 shared_ptr의 갯수를 반환
make_unique와 동일하게 make_shared로 메모리 할당 가능
shared_ptr<double> p1=make_shared<double>();
make_shared는 shared_ptr을 반환한다.
make_shared를 사용하면, 메모리에 control block과 데이터를 함께 저장하기 때문에
캐싱이 보다 효율적이다.
(make_unique는 동작에서 차이가 없지만, make_shared는 생성자를 통해 만드는 것과 차이가 있음)
shared_ptr는 참조하는 포인터의 갯수를 카운트(use_count)블록이 별도로 생성됨
(내부적으로 데이터와 control block을 포인팅하는 두 개의 포인터를 가짐)
이 때문에 shared_ptr은 원시 포인터에 비해 메모리와 실행 시간에 약간에 오버헤드가 있다.
shared_ptr은 사용자 정의 Deleter를 제공한다.
weak_ptrshared_ptr에서는 메모리 해제를 방해하는 순환 참조(Cycle Reference)가 발생할 수 있다.
(서로 상대방을 가르키는 두 개의 shared_ptr, 참조횟수가 0이 될수 없어 메모리가 해제 되지 않음)
이를 weak_ptr를 통해서 막을 수 있다.
weak_ptr은 공유하더라도 소유권을 주장하지 않는다.
from Computer Science & Engineering/C++/SmartPointer
weak_ptr은 하나 이상의 shared_ptr 객체가 참고하고 있는 객체에 접근할 수 있다.
이 때, 해당 객체의 소유자의 수에는 포함되지 않는다.
weak_ptr은 use_count() 메서드와 lock() 메서드를 사용할 수 있다.
lock() 메서드는 자신이 가리키고 있는 포인터를 반환한다.
#include <iostream>
#include <memory>
using namespace std;
int main(void){
int* arr=new int(1);
shared_ptr<int> sp1(arr);
weak_ptr<int> wp=sp1;
cout<<sp1.use_count()<<'\n';
cout<<wp.use_count()<<'\n';
if(true){
shared_ptr<int> sp2=wp.lock();
cout<<sp1.use_count()<<'\n';
cout<<wp.use_count()<<'\n';
}
cout<<sp1.use_count()<<'\n';
cout<<wp.use_count()<<'\n';
system("pause");
return 0;
}
보통 이와 같이 shared_ptr을 선언하고 이를 재참조할 때, weak_ptr을 사용해서 재참조함
레퍼런스(Reference)개체만을 참고하고 싶을 경우 사용한다.
기존 개체 혹은 하위 개체에 새로운 이름을 도입함(별칭이라고도 한다.)
int i=5;
int& j=i;
j=4;
std::cout<<"i= "<<i<<'\n';
변수 j는 i를 참고한다.(j를 변경하면, i도 변경됨. 반대도 동일)
레퍼런스를 정의할 때, 포인터와 달리 어떤 변수를 참조할 것인지 직접 선언해야 하며,
나중에 다른 변수를 참조할 수 없다.
레퍼런스는 함수 인수, 다른 개체의 부분 참조 및 뷰 구축에 매우 유용하게 사용된다.
C++ 표준에서는 포인터와 레퍼런스 사이의 절충안으로 레퍼런스와 비슷하게 동작하지만, 일부 제한을 피하는reference_wrapper 클래스를 제공함(컨테이너 내부에서 사용가능)
포인터 & 레퍼런스
특징 |
포인터 |
레퍼런스 |
정의된 위치 참조 |
|
O |
초기화 필수 |
|
O |
메모리 누수 방지 |
|
O |
개체와 같은 표기법 |
|
O |
메모리 관리 |
O |
|
주소 계산 |
O |
|
컨테이너 만들기 |
O |
|
레퍼런스를 이용해서 임의의 주소를 참조할 수 있는 방법이 존재는 함
부실 레퍼런스(Stale Reference) & 댕글링 포인터(Dangling Pointer)함수 내의 지역 변수는 함수 스코프 내에서만 유효하다.
double& square_ref(double d){
double s=d*d;
return s;
}
double* square_ptr(double d){
double s=d*d;
return &s;
}
square_ref은 더 이상 유효하지 않은 지역 변수 s를 참조하며,
square_ptr은 더 이상 유효하지 않은 지역 주소를 반환한다.
(저장되어 있는 메모리가 아직 남아 있을 수도 있지만, 매우 찾기 오류를 가져올 수 있음)
멤버 데이터를 참조할 때는, 멤버 함수에서 레퍼런스 또는 포인터를 반환해도 됨
동적으로 할당된 데이터, 함수를 호출하기 전의 데이터 또는 정적 데이터에 대한 포인터 및 레퍼런스만 반환할 것
배열용 컨테이너std::vector
std::valarray
...
표준 벡터(std::vector)<vector> 헤더에 정의
배열과 포인터는 C++ 언어의 핵심 부분이다.
std::vector는 표준 라이브러리에 속하며, 클래스 템플릿으로 구현한다.
#include <vector>
int main(void){
std::vector<float> v(3), w(3);
v[0]=1; v[1]=2, v[2]=3;
w[0]=7, w[1]=8, w[2]=9;
std::vector<float> v={1, 2, 3}, w={7, 8, 9};
}
벡터의 크기는 컴파일할 때, 알 필요가 없다.
vector.size() 메서드
목록의 길이를 반환
void vector_add(const vector<float>& v1, const vector<float>& v2, vector<float>& s){
assert(v1.size()==v2.size());
assert(v1.size()==s.size());
for(unsinged i=0; i<v1.size(); ++i) s[i]=v1[i]+v2[i];
}
C의 배열 및 포인터와 달리 벡터 인수는 크기를 알고 있기 때문에, 별도로 인자로 넘겨줄 필요가 없다.
배열의 크기 또한 템플릿을 사용해 추론할 수 있음
벡터는 복사가 가능하며, 함수를 통해 반환할 수도 있다.
vector<float> add(const vector<float>& v1, const vector<float>& v2){
assert(v1.size()==v2.size());
vector<float> s(v1.size());
for(unsigned i=0; i<v1.size(); ++i){
s[i]=v1[i]+v2[i];
}
return s;
}
int main(void){
std::vector<float> v={1, 2, 3}, w={7, 8, 9}, s=add(v, w);
}
std::vector에 산술 연산은 구현되어 있지 않음(수학적 벡터를 의미하지 않음)
std::vector는 clear(), push_back(), pop_back() 등 메서드가 정의되어 있음
valarray(std::valarray)<valarray> 헤더에 정의
#include <iostream>
#include <valarray>
int main(void){
std::valarray<float> v={1, 2, 3}, w={7, 8 ,9}, s=v+2.0f*w;
v=sin(s);
for(float x:v) std::cout<<x<<' ';
std::cout<<'\n';
}
valarray는 템플릿으로 선언한 타입과만 연산이 가능하다.
ex) valarray<float>와 int의 연산(2*w)은 컴파일 오류 발생
<valarray>에 sin() 함수등 구현되어 있음
valarray는 슬라이스 접근하는데 유리하다.
각각의 연산을 포함해 행렬과 고차 텐서를 대리 실행할 수 있다.
(하지만, 대부분의 선형 대수 연산을 지원하지는 않음)